package ijeoma.tween;

import ijeoma.easing.*;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Iterator;

import processing.core.PApplet;

public class Tween {
	public static String LINEAR_EASE_IN = "linearEaseIn";
	public static String LINEAR_EASE_OUT = "linearEaseOut";
	public static String LINEAR_EASE_BOTH = "linearEaseBoth";
	public static String QUAD_EASE_IN = "quadEaseIn";
	public static String QUAD_EASE_OUT = "quadEaseOut";
	public static String QUAD_EASE_BOTH = "quadEaseBoth";
	public static String CUBIC_EASE_IN = "cubicEaseIn";
	public static String CUBIC_EASE_BOTH = "cubicEaseBoth";
	public static String CUBIC_EASE_OUT = "cubicEaseOut";
	public static String QUART_EASE_IN = "quartEaseIn";
	public static String QUART_EASE_OUT = "quartEaseOut";
	public static String QUART_EASE_BOTH = "quartEaseBoth";
	public static String QUINT_EASE_IN = "quintEaseIn";
	public static String QUINT_EASE_OUT = "quintEaseOut";
	public static String QUINT_EASE_BOTH = "quintEaseBoth";
	public static String SINE_EASE_IN = "sineEaseIn";
	public static String SINE_EASE_OUT = "sineEaseOut";
	public static String SINE_EASE_BOTH = "sineEaseBoth";
	public static String CIRC_EASE_IN = "circEaseIn";
	public static String CIRC_EASE_OUT = "circEaseOut";
	public static String CIRC_EASE_BOTH = "circEaseBoth";
	public static String EXPO_EASE_IN = "expoEaseIn";
	public static String EXPO_EASE_OUT = "expoEaseOut";
	public static String EXPO_EASE_BOTH = "expoEaseBoth";
	public static String BACK_EASE_IN = "backEaseIn";
	public static String BACK_EASE_OUT = "backEaseOut";
	public static String BACK_EASE_BOTH = "backEaseBoth";
	public static String BOUNCE_EASE_IN = "bounceEaseIn";
	public static String BOUNCE_EASE_OUT = "bounceEaseOut";
	public static String BOUNCE_EASE_BOTH = "bounceEaseBoth";
	public static String ELASTIC_EASE_IN = "elasticEaseIn";
	public static String ELASTIC_EASE_OUT = "elasticEaseOut";
	public static String ELASTIC_EASE_BOTH = "elasticEaseBoth";

	public static String SECONDS = "seconds";
	public static String FRAMES = "frames";

	public static String REVERSE = "reverse";
	public static String NO_REVERSE = "noReverse";

	private PApplet parent;

	public String name;

	public Object tweenObject;
	public Field tweenObjectParameter;

	public float begin;
	private float change = 0;
	public float end;

	private float startTime = 0;
	public float time = 0;

	public float duration = 0;
	public String durationMode = SECONDS;

	public float delay = 0;
	private float delayStartTime = 0;
	private float delayTime = 0;

	public String easing;

	String repeatMode = NO_REVERSE;
	int repeatCount = 0;
	int repeatTimes = 0;

	public float position = 0;

	private float pauseTime = 0;

	public boolean isTweening = false;
	public boolean isRepeating = false;
	public boolean isDelaying = false;
	public boolean isPaused = false;

	private ArrayList<TweenEventListener> listeners;

	private Method tweenStartedMethod, tweenEndedMethod, tweenPausedMethod,
			tweenResumedMethod, tweenRepeatedMethod, tweenDelayStartedMethod,
			tweenDelayEndedMethod;

	public Tween(PApplet _parent, String _name, Object _tweenObject, String _tweenObjectParameterName, float _begin, float _end, float _duration, String _durationMode, float _delay, String _easing) {
		setup(_parent, _name, _begin, _end, _duration, _durationMode, _delay, _easing);
		setupObjectParameter(_tweenObject, _tweenObjectParameterName);
		setupEvents();
	}

	public Tween(PApplet _parent, String _name, Object _tweenObject, String _tweenObjectParameterName, float _begin, float _end, float _duration, String _durationMode, float _delay) {
		setup(_parent, _name, _begin, _end, _duration, _durationMode, _delay, easing);
		setupObjectParameter(_tweenObject, _tweenObjectParameterName);
		setupEvents();
	}

	public Tween(PApplet _parent, String _name, float _begin, float _end, float _duration, String _durationMode, float _delay, String _easing) {
		setup(_parent, _name, _begin, _end, _duration, _durationMode, _delay, _easing);
		setupEvents();
	}

	public Tween(PApplet _parent, String _name, float _begin, float _end, float _duration, String _durationMode, float _delay) {
		setup(_parent, _name, _begin, _end, _duration, _durationMode, _delay, easing);
		setupEvents();
	}

	public Tween(PApplet _parent, String _name, float _begin, float _end, float _duration, String _durationMode) {
		setup(_parent, _name, _begin, _end, _duration, _durationMode, delay, easing);
		setupEvents();
	}

	public Tween(PApplet _parent, String _name, float _begin, float _end, float _duration) {
		setup(_parent, _name, _begin, _end, _duration, durationMode, delay, easing);
		setupEvents();
	}

	public Tween(PApplet _parent, String _name, float _begin, float _end, float _duration, float _delay) {
		setup(_parent, _name, _begin, _end, _duration, durationMode, _delay, easing);
		setupEvents();
	}

	private void setup(PApplet _parent, String _name, float _begin, float _end, float _duration, String _durationMode, float _delay, String _easing) {
		parent = _parent;

		name = _name;

		begin = _begin;
		end = _end;
		position = _begin;

		duration = _duration;
		durationMode = _durationMode;

		change = end - begin;

		delay = _delay;

		easing = _easing;

		listeners = new ArrayList<TweenEventListener>();
	}

	private void setupObjectParameter(Object _tweenObject, String _tweenObjectParameterName) {
		tweenObject = _tweenObject;

		try {
			tweenObjectParameter = _tweenObject.getClass().getField(_tweenObjectParameterName);
		} catch (SecurityException e) {
			e.printStackTrace();
		} catch (NoSuchFieldException e) {
			e.printStackTrace();
		}
	}

	private void setupEvents() {
		try {
			Class<? extends PApplet> parentClass = parent.getClass();
			tweenStartedMethod = parentClass.getMethod(TweenEvent.TWEEN_STARTED + "Event", new Class[] { Tween.class });
			tweenEndedMethod = parentClass.getMethod(TweenEvent.TWEEN_ENDED + "Event", new Class[] { Tween.class });
			tweenPausedMethod = parentClass.getMethod(TweenEvent.TWEEN_PAUSED + "Event", new Class[] { Tween.class });
			tweenResumedMethod = parentClass.getMethod(TweenEvent.TWEEN_RESUMED + "Event", new Class[] { Tween.class });
			tweenRepeatedMethod = parentClass.getMethod(TweenEvent.TWEEN_REPEATED + "Event", new Class[] { Tween.class });
			tweenDelayStartedMethod = parentClass.getMethod(TweenEvent.TWEEN_DELAY_STARTED + "Event", new Class[] { Tween.class });
			tweenDelayEndedMethod = parentClass.getMethod(TweenEvent.TWEEN_DELAY_ENDED + "Event", new Class[] { Tween.class });
		} catch (Exception e) {

		}
	}

	public void start() {
		if (delay > 0 && delay != delayTime) {
			isDelaying = true;

			delayStartTime = (durationMode == SECONDS) ? parent.millis() : parent.frameCount;

			dispatchTweenDelayStartedEvent();
		} else {
			isTweening = true;
			seek(0);

			dispatchTweenStartedEvent();
		}
	}

	public void end() {
		isTweening = false;
		dispatchTweenEndedEvent();
	}

	public void pause() {
		dispatchTweenPausedEvent();
		isPaused = true;
		isTweening = false;
		pauseTime = time;
	}

	public void resume() {
		if (isPaused) {
			isPaused = false;
			isTweening = true;
			seek(pauseTime);
			dispatchTweenResumedEvent();
		}
	}

	public void seek(float _time) {
		time = _time;
		startTime = (durationMode == SECONDS) ? (parent.millis() - time * 1000) : (parent.frameCount - time);
	}

	public void reverse() {
		float beginTemp = begin;
		float endTemp = end;

		begin = endTemp;
		end = beginTemp;
		change = end - begin;

		start();
	}

	public void repeat() {
		isRepeating = true;
	}

	public void repeat(String _repeatMode) {
		isRepeating = true;
		repeatMode = _repeatMode;
	}

	public void noRepeat() {
		repeatTimes = 0;
	}

	public void update() {
		if (isDelaying) {
			delayTime = (durationMode == SECONDS) ? (parent.millis() - delayStartTime) / 1000 : (parent.frameCount - delayStartTime);

			if (delayTime >= delay) {
				dispatchTweenDelayEndedEvent();
				isDelaying = false;
				start();
			}
		} else if (isTweening) {
			setTime(getTime());

			if (time >= duration) {
				if (isRepeating) {
					dispatchTweenRepeatedEvent();

					PApplet.println(repeatTimes + " == 0 || " + repeatCount + " <= " + repeatTimes);
					if (repeatTimes == 0 || repeatCount < repeatTimes - 1) {
						if (repeatMode == REVERSE)
							reverse();

						start();

						repeatCount++;
					} else {
						isRepeating = false;
					}
				} else {
					time = duration;
					end();
				}
			}

			setPosition(getPosition());
			setObjectParameter();
		}
	}

	private void setTime(float _time) {
		time = _time;
	}

	public float getTime() {
		return (durationMode == SECONDS) ? ((parent.millis() - startTime) / 1000) : (parent.frameCount - startTime);
	}

	public void setPosition(float _position) {
		position = _position;
	}

	public float getPosition() {
		return getPosition(getTime());
	}

	public float getPosition(float _time) {
		float position = 0;

		if (easing == LINEAR_EASE_IN)
			position = Linear.easeIn(_time, begin, change, duration);
		else if (easing == LINEAR_EASE_OUT)
			position = Linear.easeOut(_time, begin, change, duration);
		else if (easing == LINEAR_EASE_BOTH)
			position = Linear.easeInOut(_time, begin, change, duration);
		else if (easing == QUAD_EASE_IN)
			position = Quad.easeIn(_time, begin, change, duration);
		else if (easing == QUAD_EASE_OUT)
			position = Quad.easeOut(_time, begin, change, duration);
		else if (easing == QUAD_EASE_BOTH)
			position = Quad.easeInOut(_time, begin, change, duration);
		else if (easing == CUBIC_EASE_IN)
			position = Cubic.easeIn(_time, begin, change, duration);
		else if (easing == CUBIC_EASE_OUT)
			position = Cubic.easeOut(_time, begin, change, duration);
		else if (easing == CUBIC_EASE_BOTH)
			position = Cubic.easeInOut(_time, begin, change, duration);
		else if (easing == QUART_EASE_IN)
			position = Quart.easeIn(_time, begin, change, duration);
		else if (easing == QUART_EASE_OUT)
			position = Quart.easeOut(_time, begin, change, duration);
		else if (easing == QUART_EASE_BOTH)
			position = Quart.easeInOut(_time, begin, change, duration);
		else if (easing == QUINT_EASE_IN)
			position = Quint.easeIn(_time, begin, change, duration);
		else if (easing == QUINT_EASE_OUT)
			position = Quint.easeOut(_time, begin, change, duration);
		else if (easing == QUINT_EASE_BOTH)
			position = Quint.easeInOut(_time, begin, change, duration);
		else if (easing == SINE_EASE_IN)
			position = Sine.easeIn(_time, begin, change, duration);
		else if (easing == SINE_EASE_OUT)
			position = Sine.easeOut(_time, begin, change, duration);
		else if (easing == SINE_EASE_BOTH)
			position = Sine.easeInOut(_time, begin, change, duration);
		else if (easing == CIRC_EASE_IN)
			position = Circ.easeIn(_time, begin, change, duration);
		else if (easing == CIRC_EASE_OUT)
			position = Circ.easeOut(_time, begin, change, duration);
		else if (easing == CIRC_EASE_BOTH)
			position = Circ.easeInOut(_time, begin, change, duration);
		else if (easing == EXPO_EASE_IN)
			position = Expo.easeIn(_time, begin, change, duration);
		else if (easing == EXPO_EASE_OUT)
			position = Expo.easeOut(_time, begin, change, duration);
		else if (easing == EXPO_EASE_BOTH)
			position = Expo.easeInOut(_time, begin, change, duration);
		else if (easing == BACK_EASE_IN)
			position = Back.easeIn(_time, begin, change, duration);
		else if (easing == BACK_EASE_OUT)
			position = Back.easeOut(_time, begin, change, duration);
		else if (easing == BACK_EASE_BOTH)
			position = Back.easeInOut(_time, begin, change, duration);
		else if (easing == BOUNCE_EASE_IN)
			position = Bounce.easeIn(_time, begin, change, duration);
		else if (easing == BOUNCE_EASE_OUT)
			position = Bounce.easeOut(_time, begin, change, duration);
		else if (easing == BOUNCE_EASE_BOTH)
			position = Bounce.easeInOut(_time, begin, change, duration);
		else if (easing == ELASTIC_EASE_IN)
			position = Elastic.easeIn(_time, begin, change, duration);
		else if (easing == ELASTIC_EASE_OUT)
			position = Elastic.easeOut(_time, begin, change, duration);
		else if (easing == ELASTIC_EASE_BOTH)
			position = Elastic.easeInOut(_time, begin, change, duration);

		return position;
	}

	private void setObjectParameter() {
		if (tweenObjectParameter != null) {
			try {
				tweenObjectParameter.setFloat(tweenObject, position);
			} catch (IllegalArgumentException e) {
				e.printStackTrace();
			} catch (IllegalAccessException e) {
				e.printStackTrace();
			}
		}
	}

	public void setBegin(float _begin) {
		begin = _begin;
		change = end - begin;
	}

	public float getBegin() {
		return begin;
	}

	public void setEnd(float _end) {
		end = _end;
		change = end - begin;
	}

	public float getEnd() {
		return end;
	}

	public void setDuration(float _duration) {
		duration = _duration;
	}

	public float getDuration() {
		return duration;
	}

	public void setEasing(String _easing) {
		easing = _easing;
	}

	public String getEasing() {
		return easing;
	}

	public void setDurationMode(String _durationMode) {
		durationMode = _durationMode;
	}

	public String getDurationMode() {
		return durationMode;
	}

	public void setRepeatMode(String _repeatMode) {
		repeatMode = _repeatMode;
	}

	public String getRepeatMode() {
		return repeatMode;
	}

	public void setRepeatTimes(int _repeatTimes) {
		repeatTimes = _repeatTimes;
	}

	public int getRepeatTimes() {
		return repeatTimes;
	}

	public Tween addEventListener(TweenEventListener listener) {
		listeners.add(listener);
		return this;
	}

	public void removeEventListener(TweenEventListener listener) {
		listeners.remove(listener);
	}

	private synchronized void dispatchEvent(String eventType) {
		TweenEvent te = new TweenEvent(this, eventType);

		Iterator<TweenEventListener> ls = listeners.iterator();

		while (ls.hasNext()) {
			ls.next().onTweenEvent(te);
		}
	}

	private void dispatchTweenStartedEvent() {
		if (tweenStartedMethod != null) {
			try {
				tweenStartedMethod.invoke(parent, new Object[] { this });
			} catch (Exception e) {
				e.printStackTrace();
				tweenStartedMethod = null;
			}
		}

		dispatchEvent(TweenEvent.TWEEN_STARTED);
	}

	private void dispatchTweenEndedEvent() {
		if (tweenEndedMethod != null) {
			try {
				tweenEndedMethod.invoke(parent, new Object[] { this });
			} catch (Exception e) {
				e.printStackTrace();
				tweenEndedMethod = null;
			}
		}

		dispatchEvent(TweenEvent.TWEEN_ENDED);
	}

	private void dispatchTweenPausedEvent() {
		if (tweenPausedMethod != null) {
			try {
				tweenPausedMethod.invoke(parent, new Object[] { this });
			} catch (Exception e) {
				e.printStackTrace();
				tweenPausedMethod = null;
			}
		}

		dispatchEvent(TweenEvent.TWEEN_PAUSED);
	}

	private void dispatchTweenResumedEvent() {
		if (tweenResumedMethod != null) {
			try {
				tweenResumedMethod.invoke(parent, new Object[] { this });
			} catch (Exception e) {
				e.printStackTrace();
				tweenResumedMethod = null;
			}
		}

		dispatchEvent(TweenEvent.TWEEN_RESUMED);
	}

	private void dispatchTweenRepeatedEvent() {
		if (tweenRepeatedMethod != null) {
			try {
				tweenRepeatedMethod.invoke(parent, new Object[] { this });
			} catch (Exception e) {
				e.printStackTrace();
				tweenRepeatedMethod = null;
			}
		}

		dispatchEvent(TweenEvent.TWEEN_REPEATED);
	}

	private void dispatchTweenDelayStartedEvent() {
		if (tweenDelayStartedMethod != null) {
			try {
				tweenDelayStartedMethod.invoke(parent, new Object[] { this });
			} catch (Exception e) {
				e.printStackTrace();
				tweenDelayStartedMethod = null;
			}
		}

		dispatchEvent(TweenEvent.TWEEN_DELAY_STARTED);
	}

	private void dispatchTweenDelayEndedEvent() {
		if (tweenDelayEndedMethod != null) {
			try {
				tweenDelayEndedMethod.invoke(parent, new Object[] { this });
			} catch (Exception e) {
				e.printStackTrace();
				tweenDelayEndedMethod = null;
			}
		}

		dispatchEvent(TweenEvent.TWEEN_DELAY_ENDED);
	}

	public String toString() {
		return ("Tween[name: " + name + " begin: " + begin + " end: " + end + " duration: " + duration + " delay: " + delay + " easing: " + easing + "]");
	}
}
